/*
 * Copyright (c) 2013-2013 XXXX XXXXXX, Inc.
 * XXXX Road, XXXX, XXXX State, XXXX Post code, XXXX Country.
 * All rights reserved.
 *
 * This software is the confidential and proprietary information of XXXX
 * XXXXXX, Inc. ("Confidential Information").  You shall not
 * disclose such Confidential Information and shall use it only in
 * accordance with the terms of the license agreement you entered into
 * with XXXX.
 * 
 * History
 * Date        Ver Author        Change Description
 * ----------- --- ------------- ----------------------------------------
 * XXXXXXXXXXX 001 Unknown       Create
 * 28 May 2013 002 Karl          Add method to wait for expected condition
 * 08 Jun 2013 003 Karl          Add method to wait for list to load completely
 */
package test.com.sms.selenium.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.openqa.selenium.Alert;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.NoSuchFrameException;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.Point;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.TimeoutException;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriver.TargetLocator;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.ie.InternetExplorerDriver;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.remote.Augmenter;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.safari.SafariDriver;
import org.openqa.selenium.support.events.WebDriverEventListener;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.Select;
import org.openqa.selenium.support.ui.WebDriverWait;
import org.openqa.selenium.remote.RemoteWebDriver;

import com.thoughtworks.selenium.SeleneseTestBase;


public class SeleniumUtil {
	
	protected static final String TEST_BROWSER = Browsers.FIREFOX;
	
	/**
	 * Max waiting time for page elements to be loaded is set to be 10 seconds 
	 * and the time interval is 1 second.
	 */
	private static final long DEFAULT_TIMEOUT = 30000; 
	private static final long DEFAULT_WAIT = 1000; 
	
	// cached elements
	private static Map<String, Object> elements = new HashMap<String, Object>();

	/**
	 * implicitly wait time is 30 seconds.
	 */
	private static final int IMPLICITLY_WAIT = 30; // 30 second
	
	private static WebDriver driver = null;
	private static Actions action = null;

	private static final SeleneseTestBase testbase = new SeleneseTestBase();
	

	/**
	 * The following system properties can be passed to UT at the startup:
	 * -DseleniumHost -DseleniumPort
	 */
	private static String seleniumHost = System.getProperty("seleniumHost");
	private static String seleniumPort = System.getProperty("seleniumPort");


	/**
	 * The following system properties can be passed to UT at the startup:
	 * -Dbrowser
	 */
	private static String browser = System.getProperty("browser", TEST_BROWSER);

	private enum IsMethod {
		IS_ENABLED, IS_DISPLAYED, IS_SELECTED
	}
	
	public static final WebDriver startWebDriver() {
		 driver = loadDriver(browser);
		 action = new Actions(driver);
		 return driver;
	}
	

	@SuppressWarnings("unused")
	private static class MyEventListener implements WebDriverEventListener {
		public void onException(Throwable ex, WebDriver dr) {
			System.out.println(getImgPath()+"\n"+generateImgName(ex));
			catchScreenshot(getImgPath(), generateImgName(ex));
		}

		@Override
		public void afterChangeValueOf(WebElement arg0, WebDriver arg1) {

		}

		@Override
		public void afterClickOn(WebElement arg0, WebDriver arg1) {

		}

		@Override
		public void afterFindBy(By arg0, WebElement arg1, WebDriver arg2) {

		}

		@Override
		public void afterNavigateBack(WebDriver arg0) {

		}

		@Override
		public void afterNavigateForward(WebDriver arg0) {
			

		}

		@Override
		public void afterNavigateTo(String arg0, WebDriver arg1) {
			

		}

		@Override
		public void afterScript(String arg0, WebDriver arg1) {
			

		}

		@Override
		public void beforeChangeValueOf(WebElement arg0, WebDriver arg1) {
			

		}

		@Override
		public void beforeClickOn(WebElement arg0, WebDriver arg1) {
			

		}

		@Override
		public void beforeFindBy(By arg0, WebElement arg1, WebDriver arg2) {
			

		}

		@Override
		public void beforeNavigateBack(WebDriver arg0) {
			

		}

		@Override
		public void beforeNavigateForward(WebDriver arg0) {
			

		}

		@Override
		public void beforeNavigateTo(String arg0, WebDriver arg1) {
			

		}

		@Override
		public void beforeScript(String arg0, WebDriver arg1) {
			

		}
	}
		
	private static WebDriver loadDriver(String browser) {
	    if (seleniumHost != null && !"".equals(seleniumHost)) {
            DesiredCapabilities capabilities = new DesiredCapabilities();
            capabilities.setBrowserName(getBrowser());

            try {
                return new RemoteWebDriver(new URL("http://" + seleniumHost + ":" + getSeleniumPort() + "/wd/hub"), capabilities);
            } catch (MalformedURLException e) {
                
            }
        } else if (!("".equals(browser)) && "ie".equals(browser)) {
			System.setProperty("webdriver.ie.driver","C:\\IEDriverServer.exe");
			DesiredCapabilities ieCapabilities = DesiredCapabilities.internetExplorer();
		    ieCapabilities.setCapability(InternetExplorerDriver.INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS,true);
			return new InternetExplorerDriver(ieCapabilities);
		} else if (!("".equals(browser)) && "firefox".equals(browser))
			return new FirefoxDriver();
		else if (!("".equals(browser)) && "safari".equals(browser)) {
			return new SafariDriver();

		} else if (!("".equals(browser)) && "chrome".equals(browser)) {
			System.setProperty("webdriver.chrome.driver",
					"C:\\chromedriver.exe");
			return new ChromeDriver();
		} 

		return new FirefoxDriver();
	}
	
	/**
	 * The logic of waiting elements to load.
	 */
	@SuppressWarnings("rawtypes")
	private static final boolean isExecuted(Enum isMethod, WebElement element) {
		long timer = 0;
		while (true) {
			try {
				if ((IsMethod.IS_ENABLED.equals(isMethod) && element.isEnabled())
						|| (IsMethod.IS_DISPLAYED.equals(isMethod) && element.isDisplayed())
						|| (IsMethod.IS_SELECTED.equals(isMethod) && element.isSelected())) {
					return true;
				} else {
					Thread.sleep(DEFAULT_WAIT);
				}
			} catch (Exception e) {
			}
			timer += DEFAULT_WAIT;
			if (timer > DEFAULT_TIMEOUT) {
				break;
			}
		}
		return false;
	}

	public static int getSeleniumPort() {
		if (seleniumPort == null) {
			seleniumPort = "4444"; // default selenium port
		}
		return Integer.parseInt(seleniumPort);
	}

	public static String getBrowser() {
		if (browser == null)
			browser = "*iexplore"; // default browser

		return browser;
	}

	public static final void get(String baseUrl) {
		driver.get(baseUrl);
	}
	
	public static final void quit() {
		driver.quit();
	}

	public static final WebElement findElement(By by) {
		return driver.findElement(by);
	}
	
	public static final WebElement findElement(WebElement element, By by) {
		return element.findElement(by);
	}
	
	public static final List<WebElement> findElements(By by) {
		return driver.findElements(by);
	}
	
	public static final List<WebElement> findElements(WebElement element, By by) {
		return element.findElements(by);
	}
	
	public static final String getCurrentUrl() {
		return driver.getCurrentUrl();
	}
	
	public static final Point getPosition() {
		return driver.manage().window().getPosition();
	}

	public static final String getPageSource() {
		return driver.getPageSource();
	}
	
	public static final WebDriver.Options manage() {
		return driver.manage();
	}
	
	public static final void refresh() {
		driver.navigate().refresh();
	}
	
	public static final void maximize() {
		driver.manage().window().maximize();
	}

	public static final void implicitlyWait() {
		driver.manage().timeouts().implicitlyWait(IMPLICITLY_WAIT, TimeUnit.SECONDS);
	}
	
	public static final void sendKeys(WebElement element, String keys) {
		element.sendKeys(keys);
	}
	
	// make sure that all scripts are in sequence
	public static final void executeScript(String ...scripts) {
		JavascriptExecutor js;
		if (driver instanceof JavascriptExecutor) {
			js = (JavascriptExecutor) driver;
			for (String script : scripts)
				js.executeScript(script);
		}
	}
	
	public static final Alert alert() {
		return driver.switchTo().alert();
	}

	public static final void clear(WebElement element) {
		element.clear();
	}
	
	public static final void click(WebElement element) {
		element.click();
	}	
	
	public static final void selectByVisibleText(Select select, String text){
		select.selectByVisibleText(text);
	}
	
	public static final boolean isDisplayed(WebElement element) {
		return isExecuted(IsMethod.IS_DISPLAYED, element);
	}
	
	public static final boolean isEnabled(WebElement element) {
		return isExecuted(IsMethod.IS_ENABLED, element);

	}

	public static final boolean isSelected(WebElement element) {
		return isExecuted(IsMethod.IS_SELECTED, element);

	}
	
	public static final void waitPageToLoad(WebElement element) {
       assertTrue(isDisplayed(element));
	}
	
	public static final Actions actions_movetoElement(WebElement toElement) {
		return action.moveToElement(toElement);
	}
	
	public static final Actions actions_click() {
		return action.click();
	}
	
	public static final void actions_perform() {
		action.perform();
	}
	
	/**Like JUnit's Assert.assertEquals, compare two objects */
	public static final void assertEquals(Object expected, Object actual) {
		SeleneseTestBase.assertEquals(expected, actual);
	}

	/**Like JUnit's Assert.assertEquals, compare two strings */
	public static final void assertEquals(String expected, String actual) {
		SeleneseTestBase.assertEquals(expected, actual);
	}

	/**Like JUnit's Assert.assertEquals, compare two string arrays */
	public static final void assertEquals(String[] expected, String[] actual) {
		SeleneseTestBase.assertEquals(expected, actual);
	}
	
	/**Like JUnit's Assert.assertTrue, if true then pass*/
	public static final void assertTrue(boolean condition) {
		SeleneseTestBase.assertTrue(condition);
	}
	
	/**Like JUnit's Assert.assertTrue with message, if true then pass*/
	public static final void assertTrue(String message, boolean condition) {
		SeleneseTestBase.assertTrue(message, condition);
	}
	
	/**Like JUnit's Assert.assertFalse, if false then pass*/
	public static final void assertFalse(boolean condition) {
		SeleneseTestBase.assertFalse(condition);
	}

	/**Asserts that two boolean values are not the same*/
	public static final void assertNotEquals(boolean expected, boolean actual) {
		SeleneseTestBase.assertNotEquals(expected, actual);
	}

	/**Asserts that two objects are not the same*/
	public static final void assertNotEquals(Object expected, Object actual) {
		SeleneseTestBase.assertNotEquals(expected, actual);
	}

	/**Like assertEquals, but fails at the end of the test (during tearDown)*/
	public static final void verifyEquals(Object expected, Object actual) {
		testbase.verifyEquals(expected, actual);
	}

	/**Like assertEquals, but fails at the end of the test (during tearDown)*/
	public static final void verifyEquals(boolean expected, boolean actual) {
		testbase.verifyEquals(expected, actual);
	}
	
	/**Like assertFalse, but fails at the end of the test (during tearDown)*/
	public static final void verifyFalse(boolean actual) {
		testbase.verifyFalse(actual);
	}

	/**Like assertTrue, but fails at the end of the test (during tearDown)*/
	public static final void verifyTrue(boolean actual) {
		testbase.verifyTrue(actual);
	}

	/**Like assertNotEquals, but fails at the end of the test (during tearDown)*/
	public static final void verifyNotEquals(Object expected, Object actual) {
		testbase.verifyNotEquals(expected, actual);
	}
	
	/**when the system running is not expect, break running and report a error message*/
	@SuppressWarnings("static-access")
	public static final void fail(String errorMessage){
		testbase.fail(errorMessage);
	}
	
	public static void getScreenShot(Exception e) {
		 getScreenShot();
		 throw new WebDriverException(e);
	}
	
	private static String path = System.getProperty("path_for_error_screenshots", "c:/img");
	
	private static void getScreenShot() {
		Calendar date = Calendar.getInstance();
		int year = date.get(Calendar.YEAR);
		int month = date.get(Calendar.MONTH);
		int day = date.get(Calendar.DATE);
		int hour = date.get(Calendar.HOUR);
		int min = date.get(Calendar.MINUTE);
		int sec = date.get(Calendar.SECOND);
		int msec = date.get(Calendar.MILLISECOND);
		String today = "" + year + "" + month + "" + day + "" + hour + "" + min
				+ "" + sec + "" + msec;
		File screenshot = null;
		if (!(driver instanceof TakesScreenshot)) {
			WebDriver augmentDriver = new Augmenter().augment(driver);
			screenshot = ((TakesScreenshot) augmentDriver)
					.getScreenshotAs(OutputType.FILE);
		} else
			screenshot = ((TakesScreenshot) SeleniumUtil.driver)
					.getScreenshotAs(OutputType.FILE);
		FileInputStream fis;

		try {
			File dic = new File(path);
			fis = new FileInputStream(screenshot);
			if (!dic.exists() && !dic.isDirectory()) {
				dic.mkdir();
			}
			File caseDic = new File(path + "/" + getRunningCaseName() + "/");
			fis = new FileInputStream(screenshot);
			if (!caseDic.exists() && !caseDic.isDirectory()) {
				caseDic.mkdir();
			}
			FileOutputStream fos = new FileOutputStream(path + "/"
					+ getRunningCaseName() + "/" + today + ".png");

			FileChannel fcin = fis.getChannel();
			FileChannel fcout = fos.getChannel();
			fcin.transferTo(0, fcin.size(), fcout);
			fcin.close();
			fcout.close();
			fis.close();
			fos.close();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** get the Base64EncodedScreenshot*/
	private static  void catchScreenshot(String imgPath, String imgName) {
		File screenshot = null;
		FileInputStream fis= null;
		FileOutputStream fos= null;
		FileChannel fcin= null;
		FileChannel fcout= null;
		File path= null;
		if (!(driver instanceof TakesScreenshot)) {
			WebDriver augmentDriver = new Augmenter().augment(driver);
			screenshot = ((TakesScreenshot) augmentDriver).getScreenshotAs(OutputType.FILE);
		} else {
			screenshot = ((TakesScreenshot) SeleniumUtil.driver).getScreenshotAs(OutputType.FILE);
			}
		path=new File(imgPath);
		if (!path.exists() && !path.isDirectory()) {
			path.mkdirs();
		}
		try {
			fis = new FileInputStream(screenshot);
			fos = new FileOutputStream(imgPath+imgName);
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
		fcin = fis.getChannel();
		fcout = fos.getChannel();
		try {
			fcin.transferTo(0, fcin.size(), fcout);
			fcout.close();
			fcin.close();
			fos.close();
			fis.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	
	/** generate name of the screenshot file*/
	private static String generateImgName(Throwable ex){
		Calendar cal=Calendar.getInstance();
		String imgName = ex.getMessage();
		int index = imgName.indexOf("\n");
		@SuppressWarnings("static-access")
		String time = cal.get(cal.YEAR)+"-"+(cal.get(cal.MONTH)+1)+"-"+cal.get(cal.DAY_OF_MONTH)+" "+cal.get(cal.HOUR_OF_DAY)+"_"+cal.get(cal.MINUTE)+"_"+cal.get(cal.SECOND); 
		imgName =imgName.substring(0, index).replaceAll("\\s", "_").replaceAll(":", "")+ ".png";
		imgName = time +"_"+imgName;
		return imgName.replaceAll("[:/\"{}]", "");
	}
	
	/** get path of the screenshot*/
	private static String getImgPath() {
		 String path="c:/screenshots/"+getRunningCaseName()+"/";
		 return path;
	}
	
	/** get name of the current running case*/
	private static String getRunningCaseName() {
		StackTraceElement stack[] = (new Throwable()).getStackTrace();
		String casename = "";
		for (int i = 0; i < stack.length; i++) {
			String temp = stack[i].getClassName();
			if (temp.endsWith("Test")) {
				casename = temp;
				break;
			}
	   }
	   casename=casename.substring(casename.lastIndexOf(".")+1);
	   return casename;
	}

	public static TargetLocator switchTo(){
		return driver.switchTo();
	}
	
	/** select default content*/
	public static void selectDefaultContent(){
		driver.switchTo().defaultContent();
	}
	
	/** select frame by index*/
	public static void selectFrame(int frameIdx){
		driver.switchTo().frame(frameIdx);
	}
	
	/** select frame by frame name or id*/
	public static void selectFrame(String locator){
		/* 
		 * chrome browser(19.0.1084.52) frame(String) not work, if frame 
		 * not found, try frame(WebElement) again. 
		 */
		try{
			driver.switchTo().frame(locator);
		} catch (NoSuchFrameException e){
			if(findElements(By.name(locator)).size() > 0){
				driver.switchTo().frame(findElement(By.name(locator)));
			} else if(findElements(By.id(locator)).size() > 0){
				driver.switchTo().frame(findElement(By.id(locator)));
			} else {
				throw e;
			}
		}
	}
	
	/** wait on specified locators*/
	public static void waitOnElements(By[] waitOnLocators){
		String message = Arrays.deepToString(waitOnLocators);
		for(final By by : waitOnLocators){
			(new WebDriverWait(driver, DEFAULT_TIMEOUT / 1000)
			  .withMessage("can not find one or more locators " + message))
			  .until(new ExpectedCondition<WebElement>(){
				@Override
				public WebElement apply(WebDriver d) {
					return d.findElement(by);
				}});
		}
	}
	
	/** wait for fixed period time by milliseconds*/
	public static void waitForFixedMilliseconds(long milliseconds){
		try {
			Thread.sleep(milliseconds);
		} catch (InterruptedException e) {
			e.printStackTrace();
			throw new RuntimeException(e);
		}
	}
	
	/** get window handler */
	public static String getWindowHandle(){
		return driver.getWindowHandle();
	}
	
	/** get window handlers */
	public static Set<String> getWindowHandles(){
		return driver.getWindowHandles();
	}
	
	/** switch to prompted window*/
	public static void switchToPromptedWindow(Set<String> before, Set<String> after){
		List<String> whs = new ArrayList<String>(after);
		whs.removeAll(before);
		if(whs.size() > 0){
			driver.switchTo().window(whs.get(0));
		}
	}
	
	/** switch to prompted window after wait for some milliseconds*/
	public static void switchToPromptedWindowAfterWait(Set<String> before, long waitForMilliseconds){
		final long ONE_ROUND_WAIT = 100;
		Set<String> after = null;
		for(long i=0; i<waitForMilliseconds; i+=ONE_ROUND_WAIT){
			after = SeleniumUtil.getWindowHandles();
			// window really prompted 
			if(after.size() > before.size()){
				break;
			} 
			SeleniumUtil.waitForFixedMilliseconds(100);
		}
		if(after != null){
			SeleniumUtil.switchToPromptedWindow(before, after);
		}
	}
	
	/** get web element innerHTML, for debug only, not allowed to show in test case*/
	public static String getInnerHtml(WebElement element){
		 return (String)((JavascriptExecutor)driver).executeScript("return arguments[0].innerHTML;", element);
	}
	
//	public static String clickWithJS(String elementid){
//		return (String)((JavascriptExecutor)driver).executeScript("window.document.getElementById('"+ elementid +"').click()");
//	}
	
	/** clear cached elements*/
	protected void clearElements(){
		elements.clear();
	}
	
	/** get WebElement by cache, and then by finding.*/
	public static WebElement getCachedElement(By by){
		WebElement element = null;
		if((element = (WebElement)elements.get(System.identityHashCode(by))) != null){
			return element;
		} else {
			element = refreshCachedElement(by);
		}
		return element;
	}
	
	/** force finding element*/
	public static WebElement refreshCachedElement(By by){
		WebElement element = SeleniumUtil.findElement(by);
		elements.put("element" + System.identityHashCode(by), element);
		return element;
	}
	
	/** get WebElements by cache, and then by finding.*/
	@SuppressWarnings({"unchecked" })
	public static List<WebElement> getCachedElements(By by){
		List<WebElement> list = null;
		if((list = (List<WebElement>)elements.get(System.identityHashCode(by))) != null){
			return list;
		} else {
			list = refreshCachedElements(by);
		}
		return list;
	}
	
	/** force find elements*/
	public static List<WebElement> refreshCachedElements(By by){
		List<WebElement> list = null;
		list = SeleniumUtil.findElements(by);
		elements.put("list" + System.identityHashCode(by), list);
		return list;
	}

	public static WebDriver getDriver() {
		return driver;
	}
	
	/** use javascript to operat the target element e*/
	public static void executeJavaScript(String js, WebElement e){
		((JavascriptExecutor)SeleniumUtil.getDriver()).executeScript(js, e);
	}
	
	/** drag the source element to target element*/
	public static void dragAndDrop(WebElement source, WebElement target){
		action.dragAndDrop(source, target);
		SeleniumUtil.waitForFixedMilliseconds(500);
		action.perform();
		SeleniumUtil.waitForFixedMilliseconds(500);
	}
	
	/**
	 * History
	 * Date        Ver Author        Change Description
	 * ----------- --- ------------- ----------------------------------------
	 * 28 May 2013 002 Karl          Add method to wait
	 * */
	public static void waitForExepectedConditionUntilTimeOutInMilliseconds(
			ExpectedCondition<Boolean> booleanCondition, int timeOutInMilliseconds,
			String message) {
		try{
			new WebDriverWait(driver, timeOutInMilliseconds / 1000).withMessage(message)
				.until(booleanCondition);
		} catch(TimeoutException e){
			e.printStackTrace();
			throw new AssertionError(message);
		}
	}

	/** 
	 * History
	 * Date        Ver Author        Change Description
	 * ----------- --- ------------- ----------------------------------------
	 * 08 Jun 2013 003 Karl          Add method to wait for list to load completely
	 */
	public static void waitForElementsToLoadCompletely(By list, int waitRound, 
		int waitMillisecondsPerRound) {
		
		final int DEFAULT_TIMEOUT_IN_MILLISECONDS = 60000;
		final int WAIT_FOR_ROUND = waitRound;
		final int WAIT_FOR_MILLISECONDS = waitMillisecondsPerRound;
		
		long beforeCallTime = System.currentTimeMillis();
		
		// if result list shows, wait for all records to load
		long prevCount = 0;
		int stableRound = 0;
		boolean someRecordsLoaded = false;
		while(true){
			long rowCount = driver.findElements(list).size();
			long endFind = System.currentTimeMillis();
			
			if(endFind - beforeCallTime > DEFAULT_TIMEOUT_IN_MILLISECONDS){
				break;
			}
			
			if(prevCount == 0 && rowCount > 0){
				someRecordsLoaded = true;
			}
			
			// if record count becomes stable, quit wait, else continue 
			if(rowCount == prevCount && someRecordsLoaded){
				stableRound ++;
				if(stableRound == WAIT_FOR_ROUND) break;
			} else {
				stableRound = 0;
			}
			prevCount = rowCount;
			waitForFixedMilliseconds(WAIT_FOR_MILLISECONDS);
		}
	}
}
